﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;

namespace KATS_Client.FileSystemWatcher
{
    /// <summary>
    /// The file system monitoring class
    /// </summary>
    public class FolderWatcher
    {
        #region Events
        /// <summary>
        /// A file system event has happened
        /// </summary>
        public static event EventHandler<FileSystemEvent> fileSystemEvent;
        /// <summary>
        /// The monitor list has changed
        /// </summary>
        private static event EventHandler<EventArgs> MonitorListChanged;
        /// <summary>
        /// The monitor exclusion list has changed
        /// </summary>
        private static event EventHandler<EventArgs> MonitorExclusionListChanged;
        #endregion

        /// <summary>
        /// This list maintains a list of all the FileSystemMonitors
        /// </summary>
        private static List<FolderWatcher> MonitorList = new List<FolderWatcher>();

        /// <summary>
        /// Holds a list of directories not to be synced.
        /// </summary>
        private static List<string> MonitorExcludedFolders = new List<string>();

        /// <summary>
        /// Restores the watchers
        /// </summary>
        private static void RestoreSystem()
        {
            LoadExcludedDirectory();
            LoadWatchers();
        }

        #region Watchers
        /// <summary>
        /// Add a folder watcher
        /// </summary>
        /// <param name="directory">The location you wish to 'watch'</param>
        /// <param name="remoteLocation">The location to corralte</param>
        public static void CreateWatcher(string directory, string remoteLocation)
        {
            MonitorList.Add(new FolderWatcher(directory, remoteLocation));
            SaveWatchers();
        }

        /// <summary>
        /// Delete a folder watcher
        /// </summary>
        /// <param name="Watcher">The folder watcher to remove</param>
        public static void DeleteWatcher(FolderWatcher Watcher)
        {
            Watcher.stop(); // stop raising events
            lock (MonitorList)// make thread safe
            {
                MonitorList.Remove(Watcher); // remove from the list
            }
            FolderWatcher.SaveWatchers(); // save the watcher list

        }

        /// <summary>
        /// Restore a folder watcher into the sytem
        /// </summary>
        /// <param name="directory">The directory to watch</param>
        /// <param name="active">Is this watcher active?</param>
        /// <param name="remoteLocation">The remote location this watcher is connected to</param>
        private static void RestoreWatcher(string directory, bool active, string remoteLocation)
        {
            FolderWatcher w = new FolderWatcher(directory, remoteLocation);
            w.setActive(active);
            lock (MonitorList) // make thread safe
            {
                MonitorList.Add(w);
            }
        }

        #endregion


        #region excluded directories
        /// <summary>
        /// Adds an excluded directory to the system
        /// </summary>
        /// <param name="directory">The directory to exclude</param>
        public static void CreateExcludedDirectory(string directory)
        {
            lock (MonitorExcludedFolders)
            {
                MonitorExcludedFolders.Add(directory);
            }
            SaveExcludedDirectory();
        }

        /// <summary>
        /// Delete an excluded directory
        /// </summary>
        /// <param name="directory">The directory to remove from the exclusion list</param>
        public static void DeleteExcludedDirectory(string directory)
        {
            for (int i = 0; i < MonitorExcludedFolders.Count; i++)
            {
                string d = MonitorExcludedFolders[i];
                if (directory == d) // if current directory matches, remove it from list
                {
                    MonitorExcludedFolders.RemoveAt(i);
                    break; // only remove one item
                }
            }
            FolderWatcher.SaveExcludedDirectory(); //  save the updated list
        }

        /// <summary>
        /// Restore a folder watcher
        /// </summary>
        /// <param name="directory">The directory to restore</param>
        private static void RestoreExcludedDirectory(string directory)
        {
            lock (MonitorExcludedFolders)
            {
                MonitorExcludedFolders.Add(directory);
            }
        }

        #endregion

        #region Load & Save Watchers
        /// <summary>
        /// Save the file systemw watchers into the appropriate xml file
        /// </summary>
        private static void SaveWatchers()
        {
            try
            {
                List<XElement> elements = new List<XElement>();
                lock (MonitorList) // lock to make threadsafe
                {
                    foreach (FolderWatcher fw in MonitorList)
                    {
                        XElement x = new XElement("Watcher",
                            new XElement("Path", fw.Path),
                            new XElement("Active", fw.Active),
                            new XElement("Remote", fw.RemoteLocation));
                        elements.Add(x);
                    }
                }

                XDocument watchersDocument =
                new XDocument(
                    new XDeclaration("1.0", "uft-8", "yes"),
                    new XComment("The folders watched by the file system monitor"),
                    new XElement("Watchers",
                    elements.ToArray()));
                watchersDocument.Save(Properties.Settings.Default.FileSystemWatcherSave);
            }
            catch (Exception e)
            {
                // catch any errors and display them on the console
                Console.WriteLine("ERROR: Could not save FileSystemWatcher's to file.\n->{0}", e.Message);
            }
        }

        /// <summary>
        /// Load the FolderWatchers
        /// </summary>
        private static void LoadWatchers()
        {
            try
            {
                XDocument document = XDocument.Load(Properties.Settings.Default.FileSystemWatcherSave);
                XElement watchers = document.Element("Watchers");

                foreach (XElement watcher in watchers.Nodes())
                {
                    // restore the watcher from the xml save
                    string path = watcher.Element("Path").Value;
                    bool active = bool.Parse(watcher.Element("Active").Value);
                    string remote = watcher.Element("Remote").Value;

                    FolderWatcher.RestoreWatcher(path, active, remote);
                }

                if (MonitorListChanged != null)
                    MonitorListChanged(new object(), new EventArgs());
            }
            catch (Exception e)
            {
                // catch any errors and display them on the console
                Console.WriteLine("ERROR: Could not load FileSystemWatcher's from file\n->{0}", e.Message);
                Console.WriteLine("{0}", e.StackTrace);
            }
        }
        #endregion

        #region Load & Save Exclusions
        /// <summary>
        /// Save the excluded directories
        /// </summary>
        private static void SaveExcludedDirectory()
        {
            try
            {
                List<XElement> elements = new List<XElement>();
                lock (MonitorExcludedFolders) // lock to make threadsafe
                {
                    foreach (string s in MonitorExcludedFolders)
                    {
                        XElement x = new XElement("Excluded", s);
                        elements.Add(x);
                    }
                }

                XDocument watchersDocument =
                     new XDocument(
                         new XDeclaration("1.0", "uft-8", "yes"),
                         new XComment("The folders excluded from being monitored by the Filesystem Monitor."),
                         new XElement("Exclusions",
                         elements.ToArray()));
                watchersDocument.Save(Properties.Settings.Default.FileSystemWatcherExcluded);

                if (MonitorExclusionListChanged != null)
                    MonitorExclusionListChanged(new object(), new EventArgs());
            }
            catch (Exception e)
            {
                // catch any errors and display them on the console
                Console.WriteLine("ERROR: Could not save FileSystemExcludedDirectory's to file\n->{0}", e.Message);
            }
        }

        /// <summary>
        /// Load the excluded directories
        /// </summary>
        private static void LoadExcludedDirectory()
        {
            try
            {
                XDocument document = XDocument.Load(Properties.Settings.Default.FileSystemWatcherExcluded);
                XElement exclusions = document.Element("Exclusions");
                foreach (XElement excluded in exclusions.Nodes())
                {
                    // restore the exclusions from the xml save
                    string directory = excluded.Value;
                    FolderWatcher.RestoreExcludedDirectory(directory);
                }
            }
            catch (Exception e)
            {
                // catch any errors and display them on the console
                Console.WriteLine("ERROR: Could not load FileSystemExcludedDirectory's from file\n->{0}", e.Message);
            }
        }
        #endregion

        /// <summary>
        /// Get the source of a Folder Watcher
        /// </summary>
        /// <param name="Directory">The directory</param>
        /// <returns>The Folder watcher if found, else null</returns>
        private static FolderWatcher GetFolderWatcher(string Directory)
        {
            lock (MonitorList)
            {
                foreach (FolderWatcher w in MonitorList)
                {
                    //Console.WriteLine("Directory: {0} :: Remote Location: {1}", directory, w.FileWatcher.Path);
                    if (Directory.Contains(w.Path))
                    {
                        return w;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Print out the subfolders for the directory provided
        /// </summary>
        /// <param name="directory">The directory</param>
        private static void PrintFolders(string directory)
        {
            DirectoryInfo di = new DirectoryInfo(directory);
            DirectoryInfo[] ds = di.GetDirectories();
            int i = 0;
            Console.WriteLine("##################################");
            Console.WriteLine("Subfolders for {0}. [{1}]", directory, ds.Length);
            foreach (DirectoryInfo d in ds)
            {
                if ((d.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
                {
                    i++;
                    Console.WriteLine("{0}: {1} - {2}", i, d.Name, d.Attributes);
                }
            }
            Console.WriteLine("##################################");
        }


        // instance level members for FolderWatcher

        /// <summary>
        /// The file system watcher for this object
        /// </summary>
        private System.IO.FileSystemWatcher FileWatcher;

        /// <summary>
        /// The path this folder watcher operates on
        /// </summary>
        private string Path
        {
            get { return FileWatcher.Path; }
        }

        /// <summary>
        /// Is this folder watcher active?
        /// </summary>
        private bool Active
        {
            get { return FileWatcher.EnableRaisingEvents; }
        }

        /// <summary>
        /// The remote location this related to
        /// </summary>
        public string RemoteLocation = string.Empty;

        /// <summary>
        /// Create a folder watcher
        /// </summary>
        /// <param name="directory">The directory to watch.</param>
        /// <param name="remoteLocation">The remote location</param>
        private FolderWatcher(string directory, string remoteLocation)
        {
            FileWatcher = new System.IO.FileSystemWatcher();
            this.RemoteLocation = remoteLocation;

            try
            {
                FileWatcher.Path = directory;
            }
            catch (ArgumentException e)
            {
                Console.WriteLine("Error: {0}", e.Message);
            }
            FileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName;

            // only watch txt files
            //watcher.Filter = "*.txt";

            // add event handlers
            FileWatcher.Changed += new FileSystemEventHandler(onChanged);
            FileWatcher.Created += new FileSystemEventHandler(onChanged);
            FileWatcher.Deleted += new FileSystemEventHandler(onChanged);
            FileWatcher.Renamed += new RenamedEventHandler(onRenamed);

            FileWatcher.IncludeSubdirectories = true;
            FileWatcher.InternalBufferSize *= 2; // double the buffer size
        }

        /// <summary>
        /// Start watching the folder
        /// </summary>
        public void start()
        {
            // begin watching the directory
            FileWatcher.EnableRaisingEvents = true;
            FolderWatcher.SaveWatchers();
        }

        /// <summary>
        /// Stop watching the folders
        /// </summary>
        public void stop()
        {
            // stop watching the directory
            FileWatcher.EnableRaisingEvents = false;
            FolderWatcher.SaveWatchers();
        }

        /// <summary>
        /// Set active?
        /// </summary>
        /// <param name="active">Active Status</param>
        private void setActive(bool active)
        {
            // set the active status
            FileWatcher.EnableRaisingEvents = active;
            FolderWatcher.SaveWatchers();
        }

        /// <summary>
        /// Handles the file changed events raised by the FileWatcher
        /// </summary>
        /// <param name="source">The source of the event</param>
        /// <param name="e">The event data</param>
        private void onChanged(object source, FileSystemEventArgs e)
        {
            // specify what is done when a file is changed, created or deleted

            System.IO.FileSystemWatcher fileWatcher = (System.IO.FileSystemWatcher)source;

            FileInfo fi = new FileInfo(e.FullPath);
            bool syncableEvent = isSyncableEvent(fi, fileWatcher, e.ChangeType);

            if (syncableEvent == true)
            {
                WatcherChangeTypes changeType;
                if (e.ChangeType == WatcherChangeTypes.Created && (fi.Attributes & FileAttributes.Directory) != FileAttributes.Directory)
                {
                    // do not do this if event is for a FOLDER
                    // if file system says a file has been created, handle it the same as a file changed event [same outcome]
                    changeType = WatcherChangeTypes.Changed;
                }
                else
                {
                    changeType = e.ChangeType;
                }
                // trigger the fileSystemEvent
                if (FolderWatcher.fileSystemEvent != null)
                {
                    FolderWatcher.fileSystemEvent(FolderWatcher.GetFolderWatcher(e.FullPath), new FileSystemEvent(e.FullPath, changeType));
                }
            }
            else
            {
                //Console.WriteLine("-File: {0} {1}! {2}", e.FullPath, e.ChangeType, fi.Attributes);
            }
        }

        /// <summary>
        /// Handles the renamed events
        /// </summary>
        /// <param name="source">The event source</param>
        /// <param name="e">The event data</param>
        private void onRenamed(object source, RenamedEventArgs e)
        {
            // specify what is done when a file is renamed.

            System.IO.FileSystemWatcher fileWatcher = (System.IO.FileSystemWatcher)source;

            FileInfo fi = new FileInfo(e.FullPath);
            bool syncableEvent = isSyncableEvent(fi, fileWatcher, e.ChangeType);

            if (syncableEvent == true)
            {
                //Console.WriteLine("File: {0} renamed to :: {1}", e.OldFullPath, e.FullPath);
                // trigger the fileSystemEvent
                if (FolderWatcher.fileSystemEvent != null)
                    FolderWatcher.fileSystemEvent(FolderWatcher.GetFolderWatcher(e.FullPath), new FileSystemEvent(e.OldFullPath, e.FullPath, e.ChangeType));
            }
            else
            {
                //Console.WriteLine("-File: {0} renamed to :: {1}", e.OldFullPath, e.FullPath);
            }
        }

        /// <summary>
        /// Is this event a event that should be synchronized?
        /// </summary>
        /// <param name="file">The file that changed</param>
        /// <param name="fileWatcher">The filesystem watcher that created this event</param>
        /// <param name="changeType">The type of change</param>
        /// <returns>To be synchronized?</returns>
        private bool isSyncableEvent(FileInfo file, System.IO.FileSystemWatcher fileWatcher, WatcherChangeTypes changeType)
        {
            FileInfo fi = file;
            DirectoryInfo di = fi.Directory;

            // go through the list of excluded folders and check this file is not contained within one of them
            lock (MonitorExcludedFolders)
            {
                foreach (string directory in MonitorExcludedFolders)
                {
                    if (file.FullName.Contains(directory))
                    {
                        // then file is inside the excluded folder
                        return false;
                    }
                }
            }
            if ((fi.Attributes & FileAttributes.Directory) == FileAttributes.Directory && fi.Attributes.ToString() != "-1")
            {
                // then 'file' is actually a folder
                // update di for this
                di = new DirectoryInfo(fi.FullName);
            }
            DirectoryInfo di_backup = new DirectoryInfo(di.FullName);

            // check if file/folder is contained within a hidden folder or reparse point
            while (di.FullName != fileWatcher.Path) // only goes back as far as the filesystemwatch's path
            {
                if ((di.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden || (di.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
                {
                    // if file is hidden or a 'Reparse point' [used for the mounted drives] do not sync them
                    return false;
                }
                di = di.Parent; // progresses onto the next parent folder
            }
            if ((fi.Attributes & FileAttributes.Directory) == FileAttributes.Directory && changeType == WatcherChangeTypes.Changed)
            {
                // if file is a directory and event is a 'changed' event, do not fire
                return false;
            }

            return true;
        }
    }
}
